// Copyright (c) 2020 Presto Labs Pte. Ltd.
// Author: fengyang

#pragma once

#include <string>

#include <rapidjson/document.h>
#include <rapidjson/rapidjson.h>

#include "coin/proto/coin_market_enums.pb.h"
#include "coin2/base/conversion.h"
#include "coin2/base/time.h"
#include "coin2/exchange/base/feed/update.h"
#include "coin2/exchange/base/feed_util/book_builder.h"
#include "coin2/exchange/base/feed_util/trade.h"

namespace coin2::exchange::bybit_futures::feed_v2 {
namespace impl {
using coin2::exchange::base::market::ExchangeType;
using coin2::exchange::base::market::MarketType;
using coin2::exchange::base::feed_util::Index;

class BybitFeedUpdateParser {
 public:
  static bool
  ParseBook(BookBuilder* book, int64_t ts, const rapidjson::Document& document,
      bool light_bbo_move_xbp) {
    const std::string& type = document["type"].GetString();
    if (type == "snapshot") {
      if (document["data"].IsArray()) {
        return BybitFeedUpdateParser::ParseSnapshot(book, ts, document["data"]);
      } else if (document["data"].HasMember("order_book")) {
        return BybitFeedUpdateParser::ParseSnapshot(
            book, ts, document["data"]["order_book"]);
      } else {
        return false;
      }
    } else if (type == "delta") {
      return BybitFeedUpdateParser::ParseDelta(book, ts, document["data"], light_bbo_move_xbp);
    } else {
      return false;
    }
  }

  static bool
  ParseSnapshot(BookBuilder* book, int64_t ts, const rapidjson::Value& book_json) {
    auto tx = book->NewTransaction(ts);
    tx.Clear();
    for (const auto& v : book_json.GetArray()) {
      auto price = stod(v["price"].GetString());
      auto qty = stod(v["size"].GetString());
      std::string side = v["side"].GetString();
      UpdateBook(side, price, qty, &tx);
    }

    return true;
  }

  static bool
  ParseDelta(BookBuilder* book, int64_t ts, const rapidjson::Value& book_json,
      bool light_bbo_move_xbp) {
    auto tx = book->NewTransaction(ts);
    if (light_bbo_move_xbp) {
      tx.BboClear();
    }
    if (book_json.HasMember("delete")) {
      for (const auto& v : book_json["delete"].GetArray()) {
        auto price = stod(v["price"].GetString());
        std::string side = v["side"].GetString();
        UpdateBook(side, price, 0LL, &tx);
      }
    }

    if (book_json.HasMember("update")) {
      for (const auto& v : book_json["update"].GetArray()) {
        auto price = stod(v["price"].GetString());
        auto qty = stod(v["size"].GetString());
        std::string side = v["side"].GetString();
        UpdateBook(side, price, qty, &tx);
      }
    }

    if (book_json.HasMember("insert")) {
      for (const auto& v : book_json["insert"].GetArray()) {
        auto price = stod(v["price"].GetString());
        auto qty = stod(v["size"].GetString());
        std::string side = v["side"].GetString();
        UpdateBook(side, price, qty, &tx);
      }
    }

    return true;
  }

  static void ParseTrade(const rapidjson::Value& trade_json, Trade* trade) {
    trade->price = stod(trade_json["price"].GetString());
    trade->fill_qty = stod(trade_json["size"].GetString());
    trade->side = ParseTradeSide(trade_json["side"].GetString());
    int64_t ts = stoll(trade_json["trade_time_ms"].GetString()) * 1'000'000LL;
    trade->timestamp = ts;
  }

  static void ParseOpenInterest(
      const rapidjson::Value& instrument_json,
      OpenInterest* open_interest,
      int64_t ts) {
    open_interest->timestamp = ts;
    if (instrument_json.HasMember("open_interest")) {
      open_interest->open_interest_qty = stod(instrument_json["open_interest"].GetString());
    } else if (instrument_json.HasMember("open_interest_e8")) {
      open_interest->open_interest_qty =
          stod(instrument_json["open_interest_e8"].GetString()) / 1e8;
    } else {
      NOTREACHED();
    }
  }

  static void ParseMarkPrice(
      const rapidjson::Value& instrument_json,
      MarkPrice* mark_price,
      int64_t ts) {
    mark_price->timestamp = ts;
    if (instrument_json.HasMember("mark_price")) {
      mark_price->mark_price = stod(instrument_json["mark_price"].GetString());
    } else {
      mark_price->mark_price = stod(instrument_json["mark_price_e4"].GetString()) / 1e4;
    }
  }

  static void ParseIndex(
      const rapidjson::Value& instrument_json,
      Index* index,
      int64_t ts) {
    index->timestamp = ts;
    if (instrument_json.HasMember("index_price")) {
      index->price = stod(instrument_json["index_price"].GetString());
    } else {
      index->price = stod(instrument_json["index_price_e4"].GetString()) / 1e4;
    }
  }

  static bool ParseFundingRate(
      const rapidjson::Value& instrument_json,
      FundingRate* funding_rate,
      int64_t ts,
      bool is_snapshot) {
    CHECK(funding_rate);
    if (is_snapshot) {
      funding_rate->Clear();
    } else if (funding_rate->timestamp == 0) {
      return false;
    }
    funding_rate->timestamp = ts;
    if (instrument_json.HasMember("funding_rate_e6")) {
      funding_rate->funding_rate = stod(instrument_json["funding_rate_e6"].GetString()) / 1e6;
    }
    if (instrument_json.HasMember("predicted_funding_rate_e6")) {
      funding_rate->estimated_rate =
          stod(instrument_json["predicted_funding_rate_e6"].GetString()) / 1e6;
    }
    if (instrument_json.HasMember("next_funding_time")) {
      funding_rate->funding_time =
          TimestampFromIso8601(instrument_json["next_funding_time"].GetString());
    }
    return true;
  }

  static bool ParseKlineUrl(
      std::string_view url,
      std::string* native_symbol,
      fastfeed::proto::KlineInterval* interval,
      int* page) {
    if (url.find("/kline") == std::string::npos) {
      return false;
    }

    std::string_view symbol_key = "symbol=";
    size_t start = url.find(symbol_key) + symbol_key.size();
    size_t end = url.find("&", start);
    *native_symbol = url.substr(start, end - start);

    std::string_view interval_key = "interval=";
    start = url.find(interval_key) + interval_key.size();
    end = url.find("&", start);
    std::string interval_str(url.substr(start, end - start));
    *interval = ParseKlineInterval(interval_str);

    *page = -1;
    std::string_view page_key = "page=";
    start = url.find(page_key);
    if (start != std::string_view::npos) {
      start += page_key.size();
      end = url.find("&", start);
      std::string_view page_view = url.substr(start, end - start);
      *page = std::stoi(std::string(page_view));
    }

    return true;
  }

  static fastfeed::proto::KlineInterval ParseKlineInterval(const std::string& period) {
    static std::map<std::string, fastfeed::proto::KlineInterval> interval_map = {
      {"1", fastfeed::proto::KLINE_INTERVAL_1MIN},
      {"3", fastfeed::proto::KLINE_INTERVAL_3MIN},
      {"5", fastfeed::proto::KLINE_INTERVAL_5MIN},
      {"15", fastfeed::proto::KLINE_INTERVAL_15MIN},
      {"30", fastfeed::proto::KLINE_INTERVAL_30MIN},
      {"60", fastfeed::proto::KLINE_INTERVAL_1HOUR},
      {"120", fastfeed::proto::KLINE_INTERVAL_2HOUR},
      {"240", fastfeed::proto::KLINE_INTERVAL_4HOUR},
      {"360", fastfeed::proto::KLINE_INTERVAL_6HOUR},
      {"720", fastfeed::proto::KLINE_INTERVAL_12HOUR},
      {"D", fastfeed::proto::KLINE_INTERVAL_1DAY},
      {"W", fastfeed::proto::KLINE_INTERVAL_1WEEK},
      {"M", fastfeed::proto::KLINE_INTERVAL_1MON}
    };
    auto iter = interval_map.find(period);
    if (iter == interval_map.end()) {
      NOTREACHED() << "Not support interval " << period;
      return fastfeed::proto::KLINE_INTERVAL_INVALID;
    }

    return iter->second;
  }

  static bool Parse1Kline(
      const rapidjson::Value& kline_data,
      Klines* klines,
      int64_t timestamp,
      fastfeed::proto::KlineInterval interval) {
    int64_t open_timestamp = stoll(kline_data["open_time"].GetString()) * 1'000'000'000L;
    auto result = klines->AddKline(open_timestamp);
    if (!result) return false;

    Kline* kline;
    bool freezed;
    std::tie(kline, freezed) = result.value();

    kline->open = stod(kline_data["open"].GetString());
    kline->high = stod(kline_data["high"].GetString());
    kline->low = stod(kline_data["low"].GetString());
    kline->close = stod(kline_data["close"].GetString());
    kline->volume = stod(kline_data["volume"].GetString());
    kline->turnover = stod(kline_data["turnover"].GetString());
    kline->timestamp = timestamp;
    kline->interval = interval;
    return freezed;
  }

 private:
  static coin2::exchange::feed::TradeSide ParseTradeSide(const std::string& side) {
    if (side == "Buy") {
      return coin2::exchange::feed::TRADE_BUY_SIDE;
    } else if (side == "Sell") {
      return coin2::exchange::feed::TRADE_SELL_SIDE;
    } else {
      throw std::runtime_error("[Bybit Feed Processor] unknown side: " + side);
    }
  }

  inline static void
  UpdateBook(const std::string& side, double price, double qty, Transaction* tx) {
    if (side == "Sell") {
      tx->UpdateAsk(price, qty);
    } else if (side == "Buy") {
      tx->UpdateBid(price, qty);
    } else {
      THROW() << "[Bybit Feed Processor] unknown side: " << side;
    }
  }
};
}  // namespace impl

using impl::BybitFeedUpdateParser;
}  // namespace coin2::exchange::bybit_futures::feed_v2
